package model;

import java.io.File;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Observable;
import java.util.Observer;

import controller.Controller.EnumEndTransfer;
import observerNotification.ChangingStateObserverNotification;
import observerNotification.ConversationCreatedObserverNotification;
import observerNotification.ErrorObserverNotification;
import observerNotification.FileAnswerObserverNotification;
import observerNotification.FileObserverNotification;
import observerNotification.FileTransferFinishedObserverNotification;
import observerNotification.MessageObserverNotification;
import observerNotification.RemoteUserObserverNotification;
import observerNotification.StartFileShareObserverNotification;
import main.ChatSystem;


/**
 * Contains data such as user list,  conversations hash map, local username
 * 
 * <p>
 * <ul>
 * <li>Project: 		ChatSystemG3</li>
 * <li>Called by: 		AbstractController</li>
 * </ul>
 * </p>
 * 
 * @author COMBES Nicolas - LECHANTRE Arnaud
 * @see Observable
 */
public class Model extends Observable{

	/**
	 * Number of the next conversation
	 */
	private static Integer nextConvNum;					
	/**
	 * List of connected users
	 */
	private ArrayList<User> userList;					
	/**
	 * Map of all the conversations
	 */
	private HashMap<Integer, Conversation> convMap;		
	/**
	 * Name of the local user 
	 */
	private String localUsername;						
	
	/**
	 * Construct 
	 * @param o An observer to add to the model (typically the ViewController)
	 */
	public Model(Observer o) {
		super();
		//We initialize the userlist
		this.userList= new ArrayList<User>();
		//initialize the conversation map
		this.convMap= new HashMap<Integer, Conversation>();
		//And we add the observer
		this.addObserver(o);
		Model.nextConvNum=1;
	}

	/**
	 * Adds the user to the userlist
	 * @param username remote user
	 * @param addr remote adresse of remote user
	 * @param isBroadcast used later on to send a hello back or not
	 */
	public void addUser(String username, InetAddress addr, boolean isBroacast) {
		//if we are in debug mode we show that the user is added to the model
		if(ChatSystem.debugMode)System.out.println("model.Model: ajout de l'utilisateur "+ username);
		//then we just add the user to the model
		this.userList.add(new RemoteUser(username, addr));
		
		//Search if we had started a conversation with the user before
		int convId=this.foundConversation(username, false);
		
		//and notify the observers
		RemoteUserObserverNotification m = new RemoteUserObserverNotification (username, addr, true, isBroacast, convId);
		this.setChanged();
		this.notifyObservers(m);
		this.clearChanged();
	}

	/**
	 * Removes the user from the userlist
	 * @param username remote user
	 * @param addr remote adresse of remote user
	 */
	public void removeUser(String username, InetAddress addr) {
		//if we are in debug mode we show that the user is removed to the model
		if(ChatSystem.debugMode)System.out.println("model.Model: retrait de l'utilisateur "+ username);
		//then we just remove the user from the userlist
		boolean trouve=false;
		int i=0;
		while(!trouve && i<this.userList.size()){
			if(this.userList.get(i).getName().equals(username)){
				this.userList.remove(i);
			}
			i++;
		}
		//and notify the observers

		int convId=this.foundConversation(username, false);
		/*if(convId!=-1){
			this.convMap.remove(convId);
		}*/
		RemoteUserObserverNotification m = new RemoteUserObserverNotification (username, addr, false, false, convId);
		this.setChanged();
		this.notifyObservers(m);
		this.clearChanged();
	}

	
	/**
	 * addMessage
	 * @param convId : identifier of the conversation in which the message must be added
	 * @param message : message to add to conversation
	 * @param username : username of the message sender
	 */
	public void addMessage(int convId, String message, String username) {
		if(ChatSystem.debugMode){
			System.out.println("model.Model: Add Message to model: param: "+ convId + "\t" + message +"\t" + username);
		}
		//If the message was sent by the local user
		if(convId!=-1){ 
			//The username may be null
			if(username==null){
				//If the username is null, the sender can only be localuser
				username=this.localUsername;	//So save his name.
			}
		} else {
			convId=foundConversation(username, true);
		}
		//And we add the message to the conversation in which it should be added
		this.convMap.get(convId).addMessage(this.getUser(username), message);
		//Finally we notify the view that a new message was added to the model
		MessageObserverNotification m= new MessageObserverNotification( convId, this.convMap.get(new Integer(convId)).getUser().getIpAddress(), username, message);
		this.setChanged();
		this.notifyObservers(m);
		this.clearChanged();
	}

	/**
	 * foundConversation
	 * @param username 
	 * @param create 
	 * @return the identifier of the conversation with the related user
	 */
	public int foundConversation (String username, boolean create){
		
		//We create an iterator for iterate the conversations 
		Iterable<Integer> mapKey =this.convMap.keySet();
		boolean found=false;
		int convId=-1;
		java.util.Iterator<Integer> iter = mapKey.iterator();
		while (iter.hasNext() && !found) {
			convId=iter.next();
			found=this.convMap.get(convId).hasRemoteUser(username);
		}
		if(found){
			//If we found the conversation (for the moment nothing to be done)
			if(ChatSystem.debugMode)System.out.println("model.Model : addConversation : conversation found");
		} 
		else if (create){
			//If conversation was not found
			if(ChatSystem.debugMode)System.out.println("model.Model : addConversation No id in param, we create the " +Model.nextConvNum+ " conversation");
			String name[]= new String[1];	//As sayed before no group conversation so only one name
			name[0]=username;
			//We ask the creation of the conversation
			this.addConversation(name, -1);
			convId= Model.nextConvNum-1;
			//And we finaly add the message to the conversation
		} else {
			return -1;
		}
		return convId;
	}
	
	/**
	 * Creates a conversation with all the given remote users
	 * @param username : array containing the names of the users participating to the conversation
	 * @param id : identifier to be assigned to the conversation (if -1 given, that means that the identifier of the conversation should be determined locally)
	 */
	public void addConversation(String[] username, int id){
		if(ChatSystem.debugMode)System.out.println("model.Model : addConversation :fuction called " );
		boolean found=false;
		//To protect the code we check that username is not empty
		if(username.length>0){
			//We create an iterator
			int key=-1;
			Iterable<Integer> mapKey =this.convMap.keySet();
			java.util.Iterator<Integer> iter = mapKey.iterator();
			while (iter.hasNext() & !found) {
				//and iterate the conversations
				key=iter.next();
				//We check if the actual conversation has exactly the same remote user list
				found=this.convMap.get(key).hasRemoteUsers(username);
			}
			//2nd add the message (username should be set)
			if(found){
				ConversationCreatedObserverNotification m = new ConversationCreatedObserverNotification( key, this.localUsername, this.convMap.get(key).getUserList());
				if(ChatSystem.debugMode)System.out.println("model.Model : addConversation :Conversation found with number"+ key );
				
				//and give the informations about the conversation
				this.setChanged();
				this.notifyObservers(m);
				this.clearChanged();
				
			} else {
				//no conversation with the user, so we create one
				RemoteUser u = ((RemoteUser)this.getUser(username[0]));
				if(!(u==null)){
					Conversation conv =new Conversation (Model.nextConvNum, u);
					this.convMap.put(Model.nextConvNum, conv);
					if(ChatSystem.debugMode)System.out.println("model.Model : addConversation :Conversation added to model with number "+ Model.nextConvNum );
					Model.nextConvNum++;
					//Notify the gui
					ConversationCreatedObserverNotification m = new ConversationCreatedObserverNotification( conv.getID(), this.localUsername, conv.getUserList());
					this.setChanged();
					this.notifyObservers(m);
					this.clearChanged();
				}
				
			}
		}
	}
	
	/**
	 * getUser
	 * @param username name of the user we are searching for
	 * @return the User named username
	 */
	private User getUser(String username) {
		int i=0;
		if(ChatSystem.debugMode)System.out.println("model.Model : getUser : username "+username);
		if(ChatSystem.debugMode)System.out.println("model.Model : getUser : userlist size " +this.userList.size());
		while (i< this.userList.size() && !(this.userList.get(i).getName().equals(username))){
			if(ChatSystem.debugMode)System.out.println("model.Model : getUser : userlistMember " +this.userList.get(i).getName());
			i++;
		}
		if(i>=this.userList.size()){
			return null;
		}
		return this.userList.get(i);
	}

	/**
	 * connect; Saves the local user name
	 * @param notif
	 */
	public void connect(ChangingStateObserverNotification notif) {
		this.localUsername = notif.getUsername();
		this.userList.add(LocalUser.getInstance(notif.getUsername()));
		this.setChanged();
		this.notifyObservers(notif);
		this.clearChanged();
	}

	/**
	 * addFileMessage
	 * @param convId conversation id in which file message is added
	 * @param file file to add to the conversation
	 * @param username name of the file sending user
	 */
	public void addFileMessage(int convId, File file, String username) {
		if(ChatSystem.debugMode){
			System.out.println("model.Model: Add file to model: param: "+ convId + "\t" + file.getName() +"\t" + username);
		}
		//If the message was sent by the local user
		if(convId!=-1){ 
			//The username may be null
			if(username==null){
				//If the username is null, the sender can only be localuser
				username=this.localUsername;	//So save his name.
			}
		} else {
			convId=foundConversation(username,true);
		}
		//And we add the message to the conversation in which it should be added
		try {
			this.convMap.get(new Integer(convId)).addFileMessage(this.getUser(username), file);
			//Finally we notify the view that a new message was added to the model
			FileObserverNotification m= new FileObserverNotification( convId, this.convMap.get(new Integer(convId)).getUser().getIpAddress(), username, file);
			this.setChanged();
			this.notifyObservers(m);
			this.clearChanged();
		} catch (Exception e) {
			ErrorObserverNotification obsError= new ErrorObserverNotification( convId, e.getMessage());
			this.setChanged();
			this.notifyObservers(obsError);
			this.clearChanged();
		}
		
		
	}

	/**
	 * Adds the request to the conversation and notifies the view
	 * @param convId identifier of the conversation 
	 * @param fileName name of the file remote user is proposing to transfer
	 * @param fileSize size of the file remote user is proposing to transfer
	 * @param username remote user
	 */
	public void addFileRequest(int convId, String fileName, long fileSize, String username) {
		if(ChatSystem.debugMode){
			System.out.println("model.Model: Add file request to model: param: "+ convId + "\t" + fileName +"\t" + username);
		}
		//If the message was sent by the local user
		if(convId!=-1){ 
			//The username may be null
			if(username==null){
				//If the username is null, the sender can only be localuser
				username=this.localUsername;	//So save his name.
			}
		} else {
			convId=foundConversation(username, true);
		}
		//And we add the message to the conversation in which it should be added
		try {
			this.convMap.get(new Integer(convId)).addFileMessage(this.getUser(username), new File(fileName));//Finally we notify the view that a new message was added to the model
			FileObserverNotification m= new FileObserverNotification( convId, this.convMap.get(new Integer(convId)).getUser().getIpAddress(), username, new File(fileName), fileSize);
			this.setChanged();
			this.notifyObservers(m);
			this.clearChanged();
		} catch (Exception e) {
			ErrorObserverNotification obsError= new ErrorObserverNotification( convId, e.getMessage());
			this.setChanged();
			this.notifyObservers(obsError);
			this.clearChanged();
		}
	}
	
	/**
	 * Saves the answer of the local user and inform the view
	 * @param convId identifier of the conversation 
	 * @param fileName name of the file to be accepted/refused
	 * @param downloadLink path to save the file
	 * @param accept local user answer
	 */
	public void answerFileRequest(int convId, String fileName, File downloadLink, boolean accept) {
		if(ChatSystem.debugMode){
			System.out.println("model.Model: Answer to file request saving in model: param: "+ convId + "\t" + fileName +"\t" + accept);
		}
		String username="";
		//If the message was sent by the local user
		if(convId!=-1){ 
			//And we add the message to the conversation in which it should be added
			this.convMap.get(new Integer(convId)).setAcceptedStateToFile(fileName, downloadLink, accept);
			username = this.localUsername;//as the other team do not send the id, if we have it, we knows who sends the file
		} 
		//Finally we notify the view that a new message was added to the model
		FileAnswerObserverNotification m= new FileAnswerObserverNotification( convId, this.convMap.get(new Integer(convId)).getUser().getIpAddress(), username ,fileName, accept, downloadLink);
		this.setChanged();
		this.notifyObservers(m);
		this.clearChanged();
	}

	/**
	 * Saves the answer of remote user to a file transfert
	 * @param convId identifier of the conversation, if -1 the function will take the conversation containing only the user names like username
	 * @param fileName name of the file to be sent
	 * @param accept answer of the user
	 * @param username name of the remote user
	 */
	public void addFileAnswer(int convId, String fileName, boolean accept, String username) {
		if(ChatSystem.debugMode){
			System.out.println("model.Model: Add answer to file request: param: "+ convId + "\t" + fileName +"\t" + username);
		}
		//If the message was sent by the local user
		if(convId!=-1){ 
			//The username may be null
			if(username==null){
				//If the username is null, the sender can only be localuser
				username=this.localUsername;	//So save his name.
			}
		} else {
			convId=foundConversation(username, true);
		}
		//And we add the message to the conversation in which it should be added
		this.convMap.get(new Integer(convId)).setAcceptedStateToFile(fileName,null, accept);
		
		//Finally we notify the view that a new message was added to the model
		StartFileShareObserverNotification m= new StartFileShareObserverNotification( convId, this.convMap.get(new Integer(convId)).getUser().getIpAddress(), username ,  this.convMap.get(new Integer(convId)).getFile(fileName), accept);
		this.setChanged();
		this.notifyObservers(m);
		this.clearChanged();
	}

	/**
	 * Notifies the view transfer is over
	 * @param convId identifier of the conversation
	 * @param endReason reason why the transfer ended
	 */
	public void informFileTransferFinished(int convId, EnumEndTransfer endReason) {
		this.convMap.get(convId).endFileTransfer();
		FileTransferFinishedObserverNotification m= new FileTransferFinishedObserverNotification( convId, this.convMap.get(new Integer(convId)).getUser().getIpAddress(), this.localUsername, endReason);
		this.setChanged();
		this.notifyObservers(m);
		this.clearChanged();
	}

	/**
	 * Empties the fields of the model
	 */
	public void disconnect() {
		//We initialize the userlist
		this.userList= new ArrayList<User>();
		//initialize the conversation map
		this.convMap= new HashMap<Integer, Conversation>();
		Model.nextConvNum=1;
	}

}
